package com.zlx.mall.basic.web.model;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.zlx.mall.basic.common.constant.Constants;
import com.zlx.mall.basic.common.exception.IllegalBizArgumentException;
import com.zlx.mall.basic.common.exception.IllegalSignatureArgumentException;
import com.zlx.mall.basic.common.holder.ValidatorHolder;
import com.zlx.mall.basic.common.model.BaseObject;
import com.zlx.mall.basic.common.model.SystemParam;
import com.zlx.mall.basic.signature.AbstractSignature;
import com.zlx.mall.basic.signature.Signature;
import com.zlx.mall.basic.signature.SignatureProperties;
import com.zlx.mall.basic.util.json.JacksonUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.concurrent.Callable;

/**
 * @author qingke
 **/
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseRequest extends BaseObject
        implements java.io.Serializable {
    private SystemParam system;
    private Long userId;

    private HttpHeaders headers;
    private URI uri;
    private MultiValueMap<String, String> uriVars;
    private String remoteAddress;
    private Map<String, String> cookies;
    private String content;

    public void build(HttpHeaders headers,
                      URI uri,
                      MultiValueMap<String, String> uriVars,
                      String remoteAddress,
                      Map<String, String> cookies,
                      String content) {
        this.headers = headers;
        this.uri = uri;
        this.uriVars = uriVars;
        this.remoteAddress = remoteAddress;
        this.cookies = cookies;
        this.content = content;
    }

    /**
     * Payload
     *
     * @see MediaType#APPLICATION_JSON
     * @see MediaType#APPLICATION_FORM_URLENCODED
     */
    @Nullable
    private JsonNode payload;

    public JsonNode getPayload() {
        if (this.payload == null) {
            this.payload = parsePayload(this.headers.getContentType(), this.content);
        }
        return this.payload;
    }

    @Nullable
    protected JsonNode parsePayload(MediaType contentType, String content) {
        if (contentType != null) {
            if (contentType.includes(MediaType.APPLICATION_JSON)) {
                return parseJson(content);
            }
            if (contentType.includes(MediaType.APPLICATION_FORM_URLENCODED)) {
                MultiValueMap<String, String> parameters = parseParameters(content);
                return JacksonUtil.convertToJsonNode(parameters);
            }
        }
        return null;
    }

    private MultiValueMap<String, String> parseParameters(String content) {
        MultiValueMap<String, String> result = new LinkedMultiValueMap<>();
        if (!StringUtils.hasText(content)) {
            return result;
        }
        StringTokenizer pairs = new StringTokenizer(content, Constants.AND);
        while (pairs.hasMoreTokens()) {
            String pair = pairs.nextToken();
            int index = pair.indexOf(Constants.EQUAL);
            if (index != -1) {
                String name = pair.substring(0, index);
                String rawValue = pair.substring(index + 1);
                for (String value : StringUtils.commaDelimitedListToStringArray(rawValue)) {
                    result.add(name, value);
                }
            } else {
                result.add(pair, Constants.EMPTY_STRING);
            }
        }
        return result;
    }

    private JsonNode parseJson(String content) {
        return JacksonUtil.toJsonNode(content);
    }

    protected <T> T wrap(Callable<T> callable) throws IllegalBizArgumentException {
        try {
            return callable.call();
        } catch (Throwable e) {
            throw new IllegalBizArgumentException(e);
        }
    }

    public <T> void validate(T target) throws IllegalBizArgumentException {
        ValidatorHolder.validate(target, IllegalBizArgumentException::new);
    }

    public <T> void signatureVerify(T target, Class<T> tClass) throws IllegalSignatureArgumentException {
        Signature signature = AnnotationUtils.getAnnotation(tClass, Signature.class);
        if (signature != null && signature.required() && target instanceof AbstractSignature) {
            if (!((AbstractSignature) target).verifySignature(SignatureProperties.getSecret(), SignatureProperties.getTimestampTtl())) {
                throw new IllegalSignatureArgumentException();
            }
        }
    }

    public <T> T toObject(Class<T> tClass) {
        return wrap(() -> JacksonUtil.convertTreeToObject(getPayload(), tClass));
    }

    public <T> T toObjectAndValidate(Class<T> tClass) {
        T target = toObject(tClass);
        validate(target);
        signatureVerify(target, tClass);
        return target;
    }

    @NotNull
    public JsonNode at(String expression) {
        // Method will never return null;
        // if no matching node exists, will return a node for which
        // {@link #isMissingNode()} returns true.
        try {
            return Optional
                    .ofNullable(getPayload())
                    .map(node -> node.at(expression))
                    .orElse(MissingNode.getInstance());

        } catch (Exception e) {
            // JsonPointer.compile error, throw IllegalArgumentException
            return MissingNode.getInstance();
        }
    }

    @Nullable
    public String asText(String expression) {
        JsonNode node = at(expression);
        if (node.isNull()) {
            // NullNode.asText() will return 'null'
            return null;
        }
        return node.asText();
    }

    public String asHasText(String expression, String errorMessage) {
        String text = asText(expression);
        if (StringUtils.hasText(text)) {
            return text;
        }
        throw new IllegalBizArgumentException(errorMessage);
    }

    @Nullable
    public <T extends Number> T asNumber(String expression, Class<T> tClass, T defaultValue) {
        JsonNode node = at(expression);

        if (node.isMissingNode() || node.isContainerNode() || node.isNull()) {
            return defaultValue;
        }

        try {
            return NumberUtils.parseNumber(node.asText(), tClass);
        } catch (IllegalArgumentException e) {
            // parseNumber throws IllegalArgumentException
            return defaultValue;
        }
    }

    public <T extends Number> T asValidNumber(String expression, Class<T> tClass, String errorMessage) {

        JsonNode node = at(expression);

        if (node.isMissingNode() || node.isContainerNode() || node.isNull()) {
            throw new IllegalBizArgumentException(errorMessage);
        }

        try {
            return NumberUtils.parseNumber(node.asText(), tClass);
        } catch (IllegalArgumentException e) {
            // parseNumber throws IllegalArgumentException
            throw new IllegalBizArgumentException(errorMessage, e);
        }
    }

    public Integer asInteger(String expression, Integer defaultValue) {
        return asNumber(expression, Integer.class, defaultValue);
    }

    @Nullable
    public Integer asInteger(String expression) {
        return asInteger(expression, null);
    }

    public Integer asValidInteger(String expression, String errorMessage) {
        return asValidNumber(expression, Integer.class, errorMessage);
    }

    public Long asLong(String expression, Long defaultValue) {
        return asNumber(expression, Long.class, defaultValue);
    }

    @Nullable
    public Long asLong(String expression) {
        return asLong(expression, null);
    }

    public Long asValidLong(String expression, String errorMessage) {
        return asValidNumber(expression, Long.class, errorMessage);
    }

    public BigDecimal asBigDecimal(String expression, BigDecimal defaultValue) {
        return asNumber(expression, BigDecimal.class, defaultValue);
    }

    @Nullable
    public BigDecimal asBigDecimal(String expression) {
        return asBigDecimal(expression, null);
    }

    public BigDecimal asValidBigDecimal(String expression, String errorMessage) {
        return asValidNumber(expression, BigDecimal.class, errorMessage);
    }

    @Nullable
    public <T> T asObject(String expression, Class<T> tClass) {
        JsonNode node = at(expression);

        if (node.isObject()) {
            return JacksonUtil.convertTreeToObject(node, tClass);
        }

        throw new IllegalBizArgumentException("Node is not object");
    }

    public <T> T asObjectAndValidate(String expression, Class<T> tClass) {
        T target = asObject(expression, tClass);
        validate(target);
        return target;
    }

    public <T> List<T> asArray(String expression, Class<T> tClass) {
        JsonNode node = at(expression);

        if (node.isArray()) {
            return JacksonUtil.convertTreeToListObject(node, tClass);
        }

        throw new IllegalBizArgumentException("Node is not array");
    }

}
